前言
这道题真心觉得出得不错,一道题学到了很多新的知识。
感谢出题人starssgo师傅和nocbtm师傅的思路和writeup,下面就来详细分析一下解题思路和其中用到的解题技巧。
存在的漏洞
off by null
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32unsigned __int64 cmd_add()
{
size_t nbyte; // [rsp+0h] [rbp-10h]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
if ( (unsigned int)sub_C16() == 1 && count <= 8 )
{
for ( HIDWORD(nbyte) = 0; SHIDWORD(nbyte) <= 8; ++HIDWORD(nbyte) )
{
if ( !ptr[SHIDWORD(nbyte)] )
{
puts("size:");
_isoc99_scanf("%d", &nbyte);
if ( (nbyte & 0x80000000) == 0LL && (signed int)nbyte <= 288 )
{
ptr[SHIDWORD(nbyte)] = malloc((signed int)nbyte);
puts("content:");
read(0, ptr[SHIDWORD(nbyte)], (unsigned int)nbyte);
*((_BYTE *)ptr[SHIDWORD(nbyte)] + (signed int)nbyte) = 0;
++count;
}
else
{
puts("sobig");
}
return __readfsqword(0x28u) ^ v2;
}
}
}
return __readfsqword(0x28u) ^ v2;
}添加用户这个函数这里,在输入完内容之后,会加个
\x00
进行截断,然而加\x00
的位置是他的size位置,超出了他的空间大小。这样就能修改下一个chunk的szie
,实现改pre_inuse
和改小下一个chunk的size
。又因为是size位置改成
\x00
,输入的size稍不注意就会错改了什么东西-_-!,这都是后话。任意地址写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29unsigned __int64 __fastcall cmd_edit(_DWORD *a1, _DWORD *a2, _DWORD *a3)
{
_DWORD *v4; // [rsp+8h] [rbp-28h]
void *buf; // [rsp+20h] [rbp-10h]
unsigned __int64 v6; // [rsp+28h] [rbp-8h]
v4 = a3;
v6 = __readfsqword(0x28u);
buf = 0LL;
if ( (unsigned int)sub_C16() == 1 )
{
if ( *a1 && *a2 && *v4 )
{
puts("addr:");
_isoc99_scanf("%ld", &buf);
puts("num:");
read(0, buf, 1uLL);
*a1 = 0;
*a2 = 0;
*v4 = 0;
puts("starssgo need ten girl friend ");
}
else
{
puts("You no flag");
}
}
return __readfsqword(0x28u) ^ v6;
}这里很容易就看出来了,输入任意地址,修改最后一个字节,且只有改一次的机会。
利用过程
以下代码块除了exp外,index都是代码块内从0算起的相对index。
0x0 leak libc address和heap address
对于libc address,要先创一个size大于80,既free
后会进入unsorted bin
的chunk,再创一个chunk垫底,防止top chunk
会跟unsorted bin
合并。删除第一个chunk进入unsorted bin
,被删除的chunk的fd
和bk
就会有指向main_arena
范围的地址。
1 | cmd_add(0xf0,'') |
然后再申请与这个unsorted bin
chunk同样大小的chunk,内容输入为空,因为我这里用的是sendline
,换行符\x0a
会覆盖fd
的最后一个字节,这里\x00
对fd
没影响。
用show函数就能输出main_arena
的地址,又因为main_arena
的地址相对libc地址的偏移是一定的,所以能够计算出libc地址。具体偏移是多少可以用gdb的vmmap命令计算出来。
1 | cmd_add(0xf0,'') |
leak heap address的思路差不多,创两个同样小的chunk,再都free
掉进fastbins
。第二个free
的chunk的fd
会指向第一free
的chunk,然后一样重新申请一个同样大小的chunk,写入内容空,再输出。
1 | cmd_add(0x10,'') |
这里输入的size要设计得当,不然一不小心就覆盖了后面chunk的size。特别后面fastbins attack时候不对齐的fake chunk,我在这里踩了不少坑。
0x01 chunk overlap
chunk overlap这部分我申请三个chunk,输入的size分别是0x40、0x68和0xf0。第一个chunk放fake chunk,第二个chunk修改fake chunk的next_size
,因为第三个chunk的size
是0x101,顺便用off by null修改第三个chunk的pre_inuse
,而第三个chunk的作用纯粹是它的pre_inuse
被修改后,根据它的pre_size
向前unlink
。
还有就是fake chunk那个要伪造下fd
和bk
指向自己,前面有了head addr,在fake chunk的0x18处放fake chunk的地址,令fake chunk->fd->bk=fake chunk
且fake chunk->bk->fd=fake chunk
就OK。
题目的edit函数非常规edit,想修改chunk得free掉再重新申请。
1 | cmd_add(0x40,flat(0,0xb1,heap_addr+0x18,heap_addr+0x20,heap_addr+0x10)) |
free第三块chunk前堆的情况:
free第三块chunk后,堆成功重叠:
这时候就可以控制第二个chunk为所欲为。
0x02 fastbins attack
经过上面的步骤,将堆的情况简单化一下就是还有一个size
为0x50的chunk且index为0,一个size
为0x70的chunk且index为1和一个大的unsorted bins
。
为了好操作一点,先申请一个chunk占着准备用来修改index为1的chunk;然后申请一个size
为0x70的chunk,并立即free掉,再free掉szie
同为0x70的index为1的chunk;最后,利用占着位的chunk修改第二个free的0x70的chunk,原本指向第一个free的0x70的chunk的fd
为想要任意读写的fake chunk的地址。
1 | cmd_add(0xc0,'') # index 2 |
此时堆中的情况:
到这里fastbins attack还没完成,由于思路的不同,后面的利用步骤会有所不同。先说说nocbtm师傅的思路。在_IO_2_1_stdin_
既stdin文件流结构体的指针里有个vtable
变量。
vtable
变量的值为_IO_file_jumps
的指针,_IO_file_jumps
中保存了一些函数指针,一系列标准IO函数中会调用这些函数指针,但_IO_file_jumps
里的内容是无法修改的,但可以修改vtable
指向伪造的_IO_file_jumps
从而getshell。
在_IO_2_1_stdin_
+ 160 - 0x3刚好能作为fake chunk的size,通过fastbins attack到这里修改vtable。
1 | fake_chunk = _IO_2_1_stdin_ + 160 - 0x3 |
这里通过one_gedget就已经getshell了。
还有另一种思路就是出题人的思路,libc的environ
里记着stack的地址。
用同样是_IO_FILE
的_IO_2_1_stdout_
,控制了其中_IO_write_base
、_IO_write_ptr
和flag,就能任意地址读取。_IO_write_base
为读取的起始地址,_IO_write_ptr
为读取的末地址,并且flag的值要得是0xfbad1800才能正常读取,至于是为什么,在后面参考的最后一条有说,但我是没看懂-_-!。在_IO_2_1_stdout_
- 0x43的地方找到个合适的fake chunk的size
,其他地方的填充就用原来的值_IO_2_1_stdout_
+ 131就行。
在修改成功之后再去_IO_2_1_stdout_
那看还是没改那样的,但是能接收到输出的,可能是输出完就恢复了。
1 | fake_chunk = _IO_2_1_stdout_ - 0x43 |
前面从environ
读栈地址就是为了改返回地址控制EIP,_IO_2_1_stdin_
的任意地址写跟_IO_2_1_stdout_
的任意地址读类似,也是需要控制flag
还有_IO_buf_base
和_IO_buf_end
。
这里说下我用非对齐的位置做这次fake chunk的size
时会出错,用出题人的方法,任意地址写的函数写一字节作为size
的方法却没问题,还有为了payload前加5\n
顺便退出程序,写入到_IO_buf_base
的返回地址要-2以接收5\n
。这回除了flag
、_IO_buf_base
和_IO_buf_end
外,其余位置用0填充即可。
1 | fake_chunk = _IO_2_1_stdin_ - 0x28 |
因为有seccomp的沙箱,改main函数的返回地址为gadget是没法getshell的。禁了些危险的syscall,只能用orw(open,read,write),所以前面content为flag的chunk是要open的文件名,和顺便用来存输出。还有要注意文件名要截断,之前没注意,怪不得一直都读不到flag-_-!。
1 | prdi = libc.search(asm("pop rdi\nret")).next() |
返回地址覆盖上了ROP,对了,这里的ROP用的是libc的。毕竟知道了libc的地址,libc的ROP偏移是一定的,ELF的好像不是,用起来比较麻烦。
贴上exp
getshell的exp
1 | # -*- coding: utf-8 -*- |
读flag的exp
1 | # -*- coding: utf-8 -*- |
后记
再说一遍,这题出得真不错,学到了很多东西,特别是关于_IO_FILE知识点。说句老话,心细挖天下,我做这题时候不够细心,以至于踩了不少坑,而且前面两个师傅水平很高,对于这题用到的技巧都很熟练了,所以写的writeup有些细节没说,我这菜鸡缺少些前置的知识,有些地方看不懂,所以写了这篇水文,也为后面的师傅填填坑。